/**
 * turn.js 4th release
 * turnjs.com
 * turnjs.com/license.txt
 *
 * Copyright (C) 2012 Emmanuel Garcia
 * All rights reserved
 */

(function ($) {
  'use strict'

  let has3d

  let hasRot

  let vendor = ''

  const version = '4.1.0'

  const PI = Math.PI

  const A90 = PI / 2

  const isTouch = 'ontouchstart' in window

  const mouseEvents = (isTouch)
    ? {
        down: 'touchstart',
        move: 'touchmove',
        up: 'touchend',
        over: 'touchstart',
        out: 'touchend',
      }
    : {
        down: 'mousedown',
        move: 'mousemove',
        up: 'mouseup',
        over: 'mouseover',
        out: 'mouseout',
      }

  // Contansts used for each corner
  //   | tl * tr |
  // l | *     * | r
  //   | bl * br |

  const corners = {
    backward: ['bl', 'tl'],
    forward: ['br', 'tr'],
    all: ['tl', 'bl', 'tr', 'br', 'l', 'r'],
  }

  // Display values

  const displays = ['single', 'double']

  // Direction values

  const directions = ['ltr', 'rtl']

  // Default options

  const turnOptions = {

    // Enables hardware acceleration

    acceleration: true,

    // Display

    display: 'double',

    // Duration of transition in milliseconds

    duration: 600,

    // First page

    page: 1,

    // Enables gradients

    gradients: true,

    // Corners used when turning the page

    turnCorners: 'bl,br',

    // Events

    when: null,
  }

  const flipOptions = {

    // Size of the active zone of each corner

    cornerSize: 100,

  }

  // Number of pages in the DOM, minimum value: 6

  const pagesInDOM = 6

  var turnMethods = {

    // Singleton constructor
    // $('#selector').turn([options]);

    init(options) {
      // Define constants

      has3d = 'WebKitCSSMatrix' in window || 'MozPerspective' in document.body.style
      hasRot = rotationAvailable()
      vendor = getPrefix()

      let i; const that = this; let pageNum = 0; const data = this.data(); const ch = this.children()

      // Set initial configuration

      options = $.extend({
        width: this.width(),
        height: this.height(),
        direction: this.attr('dir') || this.css('direction') || 'ltr',
      }, turnOptions, options)

      data.opts = options
      data.pageObjs = {}
      data.pages = {}
      data.pageWrap = {}
      data.pageZoom = {}
      data.pagePlace = {}
      data.pageMv = []
      data.zoom = 1
      data.totalPages = options.pages || 0
      data.eventHandlers = {
        touchStart: $.proxy(turnMethods._touchStart, this),
        touchMove: $.proxy(turnMethods._touchMove, this),
        touchEnd: $.proxy(turnMethods._touchEnd, this),
        start: $.proxy(turnMethods._eventStart, this),
      }

      // Add event listeners

      if (options.when) {
        for (i in options.when) {
          if (has(i, options.when))
            this.bind(i, options.when[i])
        }
      }

      // Set the css

      this.css({ position: 'relative', width: options.width, height: options.height })

      // Set the initial display

      this.turn('display', options.display)

      // Set the direction

      if (options.direction !== '')
        this.turn('direction', options.direction)

      // Prevent blue screen problems of switching to hardware acceleration mode
      // By forcing hardware acceleration for ever

      if (has3d && !isTouch && options.acceleration)
        this.transform(translate(0, 0, true))

      // Add pages from the DOM

      for (i = 0; i < ch.length; i++) {
        if ($(ch[i]).attr('ignore') != '1') {
          this.turn('addPage', ch[i], ++pageNum)
        }
      }

      // Event listeners

      $(this).bind(mouseEvents.down, data.eventHandlers.touchStart).bind('end', turnMethods._eventEnd).bind('pressed', turnMethods._eventPressed).bind('released', turnMethods._eventReleased).bind('flip', turnMethods._flip)

      $(this).parent().bind('start', data.eventHandlers.start)

      $(document).bind(mouseEvents.move, data.eventHandlers.touchMove).bind(mouseEvents.up, data.eventHandlers.touchEnd)

      // Set the initial page

      this.turn('page', options.page)

      // This flipbook is ready

      data.done = true

      return this
    },

    // Adds a page from external data

    addPage(element, page) {
      let currentPage
      let className
      let incPages = false
      const data = this.data()
      const lastPage = data.totalPages + 1

      if (data.destroying)
        return false

      // Read the page number from the className of `element` - format: p[0-9]+

      if ((currentPage = /\bp(\d+)\b/.exec($(element).attr('class'))))
        page = Number.parseInt(currentPage[1], 10)

      if (page) {
        if (page == lastPage)
          incPages = true
        else if (page > lastPage)
          throw turnError(`Page "${page}" cannot be inserted`)
      }
      else {
        page = lastPage
        incPages = true
      }

      if (page >= 1 && page <= lastPage) {
        if (data.display == 'double')
          className = (page % 2) ? ' odd' : ' even'
        else
          className = ''

        // Stop animations
        if (data.done)
          this.turn('stop')

        // Move pages if it's necessary
        if (page in data.pageObjs)
          turnMethods._movePages.call(this, page, 1)

        // Increase the number of pages
        if (incPages)
          data.totalPages = lastPage

        // Add element
        data.pageObjs[page] = $(element)
          .css({ float: 'left' })
          .addClass(`page p${page}${className}`)

        if (!hasHardPage() && data.pageObjs[page].hasClass('hard')) {
          data.pageObjs[page].removeClass('hard')
        }

        // Add page
        turnMethods._addPage.call(this, page)

        // Remove pages out of range
        turnMethods._removeFromDOM.call(this)
      }

      return this
    },

    // Adds a page

    _addPage(page) {
      const data = this.data()
      const element = data.pageObjs[page]

      if (element) {
        if (turnMethods._necessPage.call(this, page)) {
          if (!data.pageWrap[page]) {
            // Wrapper
            data.pageWrap[page] = $('<div/>', { class: 'page-wrapper', page, css: { position: 'absolute', overflow: 'hidden' } })

            // Append to this flipbook
            this.append(data.pageWrap[page])

            if (!data.pagePlace[page]) {
              data.pagePlace[page] = page
              // Move `pageObjs[page]` to wrapper
              data.pageObjs[page].appendTo(data.pageWrap[page])
            }

            // Set the size of the page
            const prop = turnMethods._pageSize.call(this, page, true)
            element.css({ width: prop.width, height: prop.height })
            data.pageWrap[page].css(prop)
          }

          if (data.pagePlace[page] == page) {
            // If the page isn't in another place, create the flip effect
            turnMethods._makeFlip.call(this, page)
          }
        }
        else {
          // Place
          data.pagePlace[page] = 0

          // Remove element from the DOM
          if (data.pageObjs[page])
            data.pageObjs[page].remove()
        }
      }
    },

    // Checks if a page is in memory

    hasPage(page) {
      return has(page, this.data().pageObjs)
    },

    // Centers the flipbook

    center(page) {
      const data = this.data()
      const size = $(this).turn('size')
      let left = 0

      if (!data.noCenter) {
        if (data.display == 'double') {
          const view = this.turn('view', page || data.tpage || data.page)

          if (data.direction == 'ltr') {
            if (!view[0])
              left -= size.width / 4
            else if (!view[1])
              left += size.width / 4
          }
          else {
            if (!view[0])
              left += size.width / 4
            else if (!view[1])
              left -= size.width / 4
          }
        }

        $(this).css({ marginLeft: left })
      }

      return this
    },

    // Destroys the flipbook

    destroy() {
      let page
      const flipbook = this
      let data = this.data()
      const events = [
        'end',
        'first',
        'flip',
        'last',
        'pressed',
        'released',
        'start',
        'turning',
        'turned',
        'zooming',
        'missing',
      ]

      if (trigger('destroying', this) == 'prevented')
        return

      data.destroying = true

      $.each(events, (index, eventName) => {
        flipbook.unbind(eventName)
      })

      this.parent().unbind('start', data.eventHandlers.start)

      $(document).unbind(mouseEvents.move, data.eventHandlers.touchMove).unbind(mouseEvents.up, data.eventHandlers.touchEnd)

      while (data.totalPages !== 0) {
        this.turn('removePage', data.totalPages)
      }

      if (data.fparent)
        data.fparent.remove()

      if (data.shadow)
        data.shadow.remove()

      this.removeData()
      data = null

      return this
    },

    // Checks if this element is a flipbook

    is() {
      return typeof (this.data().pages) == 'object'
    },

    // Sets and gets the zoom value

    zoom(newZoom) {
      const data = this.data()

      if (typeof (newZoom) == 'number') {
        if (newZoom < 0.001 || newZoom > 100)
          throw turnError(`${newZoom} is not a value for zoom`)

        if (trigger('zooming', this, [newZoom, data.zoom]) == 'prevented')
          return this

        const size = this.turn('size')
        const currentView = this.turn('view')
        const iz = 1 / data.zoom
        const newWidth = Math.round(size.width * iz * newZoom)
        const newHeight = Math.round(size.height * iz * newZoom)

        data.zoom = newZoom

        $(this).turn('stop').turn('size', newWidth, newHeight)
        /* .
          css({marginTop: size.height * iz / 2 - newHeight / 2}); */

        if (data.opts.autoCenter)
          this.turn('center')
        /* else
          $(this).css({marginLeft: size.width * iz / 2 - newWidth / 2}); */

        turnMethods._updateShadow.call(this)

        for (let i = 0; i < currentView.length; i++) {
          if (currentView[i] && data.pageZoom[currentView[i]] != data.zoom) {
            this.trigger('zoomed', [
              currentView[i],
              currentView,
              data.pageZoom[currentView[i]],
              data.zoom,
            ])

            data.pageZoom[currentView[i]] = data.zoom
          }
        }

        return this
      }
      else {
        return data.zoom
      }
    },

    // Gets the size of a page

    _pageSize(page, position) {
      const data = this.data()
      const prop = {}

      if (data.display == 'single') {
        prop.width = this.width()
        prop.height = this.height()

        if (position) {
          prop.top = 0
          prop.left = 0
          prop.right = 'auto'
        }
      }
      else {
        const pageWidth = this.width() / 2
        const pageHeight = this.height()

        if (data.pageObjs[page].hasClass('own-size')) {
          prop.width = data.pageObjs[page].width()
          prop.height = data.pageObjs[page].height()
        }
        else {
          prop.width = pageWidth
          prop.height = pageHeight
        }

        if (position) {
          const odd = page % 2
          prop.top = (pageHeight - prop.height) / 2

          if (data.direction == 'ltr') {
            prop[(odd) ? 'right' : 'left'] = pageWidth - prop.width
            prop[(odd) ? 'left' : 'right'] = 'auto'
          }
          else {
            prop[(odd) ? 'left' : 'right'] = pageWidth - prop.width
            prop[(odd) ? 'right' : 'left'] = 'auto'
          }
        }
      }

      return prop
    },

    // Prepares the flip effect for a page

    _makeFlip(page) {
      const data = this.data()

      if (!data.pages[page] && data.pagePlace[page] == page) {
        const single = data.display == 'single'
        const odd = page % 2

        data.pages[page] = data.pageObjs[page]
          .css(turnMethods._pageSize.call(this, page))
          .flip({
            page,
            next: (odd || single) ? page + 1 : page - 1,
            turn: this,
          })
          .flip('disable', data.disabled)

        // Issue about z-index
        turnMethods._setPageLoc.call(this, page)

        data.pageZoom[page] = data.zoom
      }

      return data.pages[page]
    },

    // Makes pages within a range

    _makeRange() {
      let page; let range
      const data = this.data()

      if (data.totalPages < 1)
        return

      range = this.turn('range')

      for (page = range[0]; page <= range[1]; page++)
        turnMethods._addPage.call(this, page)
    },

    // Returns a range of pages that should be in the DOM
    // Example:
    // - page in the current view, return true
    // * page is in the range, return true
    // Otherwise, return false
    //
    // 1 2-3 4-5 6-7 8-9 10-11 12-13
    //   **  **  --   **  **

    range(page) {
      let remainingPages; let left; let right; let view
      const data = this.data()

      page = page || data.tpage || data.page || 1
      view = turnMethods._view.call(this, page)

      if (page < 1 || page > data.totalPages)
        throw turnError(`"${page}" is not a valid page`)

      view[1] = view[1] || view[0]

      if (view[0] >= 1 && view[1] <= data.totalPages) {
        remainingPages = Math.floor((pagesInDOM - 2) / 2)

        if (data.totalPages - view[1] > view[0]) {
          left = Math.min(view[0] - 1, remainingPages)
          right = 2 * remainingPages - left
        }
        else {
          right = Math.min(data.totalPages - view[1], remainingPages)
          left = 2 * remainingPages - right
        }
      }
      else {
        left = pagesInDOM - 1
        right = pagesInDOM - 1
      }

      return [Math.max(1, view[0] - left), Math.min(data.totalPages, view[1] + right)]
    },

    // Detects if a page is within the range of `pagesInDOM` from the current view

    _necessPage(page) {
      if (page === 0)
        return true

      const range = this.turn('range')

      return this.data().pageObjs[page].hasClass('fixed')
        || (page >= range[0] && page <= range[1])
    },

    // Releases memory by removing pages from the DOM

    _removeFromDOM() {
      let page; const data = this.data()

      for (page in data.pageWrap) {
        if (has(page, data.pageWrap)
          && !turnMethods._necessPage.call(this, page)) {
          turnMethods._removePageFromDOM.call(this, page)
        }
      }
    },

    // Removes a page from DOM and its internal references

    _removePageFromDOM(page) {
      const data = this.data()

      if (data.pages[page]) {
        const dd = data.pages[page].data()

        flipMethods._moveFoldingPage.call(data.pages[page], false)

        if (dd.f && dd.f.fwrapper)
          dd.f.fwrapper.remove()

        data.pages[page].removeData()
        data.pages[page].remove()
        delete data.pages[page]
      }

      if (data.pageObjs[page])
        data.pageObjs[page].remove()

      if (data.pageWrap[page]) {
        data.pageWrap[page].remove()
        delete data.pageWrap[page]
      }

      turnMethods._removeMv.call(this, page)

      delete data.pagePlace[page]
      delete data.pageZoom[page]
    },

    // Removes a page

    removePage(page) {
      const data = this.data()

      // Delete all the pages
      if (page == '*') {
        while (data.totalPages !== 0) {
          this.turn('removePage', data.totalPages)
        }
      }
      else {
        if (page < 1 || page > data.totalPages)
          throw turnError(`The page ${page} doesn't exist`)

        if (data.pageObjs[page]) {
          // Stop animations
          this.turn('stop')

          // Remove `page`
          turnMethods._removePageFromDOM.call(this, page)

          delete data.pageObjs[page]
        }

        // Move the pages
        turnMethods._movePages.call(this, page, -1)

        // Resize the size of this flipbook
        data.totalPages = data.totalPages - 1

        // Check the current view

        if (data.page > data.totalPages) {
          data.page = null
          turnMethods._fitPage.call(this, data.totalPages)
        }
        else {
          turnMethods._makeRange.call(this)
          this.turn('update')
        }
      }

      return this
    },

    // Moves pages

    _movePages(from, change) {
      let page
      const that = this
      const data = this.data()
      const single = data.display == 'single'
      const move = function (page) {
        const next = page + change
        const odd = next % 2
        const className = (odd) ? ' odd ' : ' even '

        if (data.pageObjs[page]) {
          data.pageObjs[next] = data.pageObjs[page]
            .removeClass(`p${page} odd even`)
            .addClass(`p${next}${className}`)
        }

        if (data.pagePlace[page] && data.pageWrap[page]) {
          data.pagePlace[next] = next

          if (data.pageObjs[next].hasClass('fixed')) {
            data.pageWrap[next] = data.pageWrap[page]
              .attr('page', next)
          }
          else {
            data.pageWrap[next] = data.pageWrap[page]
              .css(turnMethods._pageSize.call(that, next, true))
              .attr('page', next)
          }

          if (data.pages[page]) {
            data.pages[next] = data.pages[page]
              .flip('options', {
                page: next,
                next: (single || odd) ? next + 1 : next - 1,
              })
          }

          if (change) {
            delete data.pages[page]
            delete data.pagePlace[page]
            delete data.pageZoom[page]
            delete data.pageObjs[page]
            delete data.pageWrap[page]
          }
        }
      }

      if (change > 0) {
        for (page = data.totalPages; page >= from; page--)
          move(page)
      }
      else {
        for (page = from; page <= data.totalPages; page++)
          move(page)
      }
    },

    // Sets or Gets the display mode

    display(display) {
      const data = this.data()
      const currentDisplay = data.display

      if (display === undefined) {
        return currentDisplay
      }
      else {
        if ($.inArray(display, displays) == -1)
          throw turnError(`"${display}" is not a value for display`)

        switch (display) {
          case 'single':

            // Create a temporal page to use as folded page

            if (!data.pageObjs[0]) {
              this.turn('stop')
                .css({ overflow: 'hidden' })

              data.pageObjs[0] = $('<div />', { class: 'page p-temporal' })
                .css({ width: this.width(), height: this.height() })
                .appendTo(this)
            }

            this.addClass('shadow')

            break
          case 'double':

            // Remove the temporal page

            if (data.pageObjs[0]) {
              this.turn('stop').css({ overflow: '' })
              data.pageObjs[0].remove()
              delete data.pageObjs[0]
            }

            this.removeClass('shadow')

            break
        }

        data.display = display

        if (currentDisplay) {
          const size = this.turn('size')
          turnMethods._movePages.call(this, 1, 0)
          this.turn('size', size.width, size.height)
            .turn('update')
        }

        return this
      }
    },

    // Gets and sets the direction of the flipbook

    direction(dir) {
      const data = this.data()

      if (dir === undefined) {
        return data.direction
      }
      else {
        dir = dir.toLowerCase()

        if ($.inArray(dir, directions) == -1)
          throw turnError(`"${dir}" is not a value for direction`)

        if (dir == 'rtl') {
          $(this).attr('dir', 'ltr').css({ direction: 'ltr' })
        }

        data.direction = dir

        if (data.done)
          this.turn('size', $(this).width(), $(this).height())

        return this
      }
    },

    // Detects animation

    animating() {
      return this.data().pageMv.length > 0
    },

    // Gets the current activated corner

    corner() {
      let corner
      let page
      const data = this.data()

      for (page in data.pages) {
        if (has(page, data.pages)) {
          if ((corner = data.pages[page].flip('corner'))) {
            return corner
          }
        }
      }

      return false
    },

    // Gets the data stored in the flipbook

    data() {
      return this.data()
    },

    // Disables and enables the effect

    disable(disable) {
      let page
      const data = this.data()
      const view = this.turn('view')

      data.disabled = disable === undefined || disable === true

      for (page in data.pages) {
        if (has(page, data.pages)) {
          data.pages[page].flip('disable', (data.disabled) ? true : $.inArray(Number.parseInt(page, 10), view) == -1)
        }
      }

      return this
    },

    // Disables and enables the effect

    disabled(disable) {
      if (disable === undefined) {
        return this.data().disabled === true
      }
      else {
        return this.turn('disable', disable)
      }
    },

    // Gets and sets the size

    size(width, height) {
      if (width === undefined || height === undefined) {
        return { width: this.width(), height: this.height() }
      }
      else {
        this.turn('stop')

        let page; let prop
        const data = this.data()
        const pageWidth = (data.display == 'double') ? width / 2 : width

        this.css({ width, height })

        if (data.pageObjs[0])
          data.pageObjs[0].css({ width: pageWidth, height })

        for (page in data.pageWrap) {
          if (!has(page, data.pageWrap))
            continue

          prop = turnMethods._pageSize.call(this, page, true)

          data.pageObjs[page].css({ width: prop.width, height: prop.height })
          data.pageWrap[page].css(prop)

          if (data.pages[page])
            data.pages[page].css({ width: prop.width, height: prop.height })
        }

        this.turn('resize')

        return this
      }
    },

    // Resizes each page

    resize() {
      let page; const data = this.data()

      if (data.pages[0]) {
        data.pageWrap[0].css({ left: -this.width() })
        data.pages[0].flip('resize', true)
      }

      for (page = 1; page <= data.totalPages; page++) {
        if (data.pages[page])
          data.pages[page].flip('resize', true)
      }

      turnMethods._updateShadow.call(this)

      if (data.opts.autoCenter)
        this.turn('center')
    },

    // Removes an animation from the cache

    _removeMv(page) {
      let i; const data = this.data()

      for (i = 0; i < data.pageMv.length; i++) {
        if (data.pageMv[i] == page) {
          data.pageMv.splice(i, 1)
          return true
        }
      }

      return false
    },

    // Adds an animation to the cache

    _addMv(page) {
      const data = this.data()

      turnMethods._removeMv.call(this, page)
      data.pageMv.push(page)
    },

    // Gets indexes for a view

    _view(page) {
      const data = this.data()

      page = page || data.page

      if (data.display == 'double')
        return (page % 2) ? [page - 1, page] : [page, page + 1]
      else
        return [page]
    },

    // Gets a view

    view(page) {
      const data = this.data()
      const view = turnMethods._view.call(this, page)

      if (data.display == 'double') {
        return [(view[0] > 0) ? view[0] : 0, (view[1] <= data.totalPages) ? view[1] : 0]
      }
      else {
        return [(view[0] > 0 && view[0] <= data.totalPages) ? view[0] : 0]
      }
    },

    // Stops animations

    stop(ignore, animate) {
      if (this.turn('animating')) {
        let i; let opts; let page
        const data = this.data()

        if (data.tpage) {
          data.page = data.tpage
          delete data.tpage
        }

        for (i = 0; i < data.pageMv.length; i++) {
          if (!data.pageMv[i] || data.pageMv[i] === ignore)
            continue

          page = data.pages[data.pageMv[i]]
          opts = page.data().f.opts

          page.flip('hideFoldedPage', animate)

          if (!animate)
            flipMethods._moveFoldingPage.call(page, false)

          if (opts.force) {
            opts.next = (opts.page % 2 === 0) ? opts.page - 1 : opts.page + 1
            delete opts.force
          }
        }
      }

      this.turn('update')

      return this
    },

    // Gets and sets the number of pages

    pages(pages) {
      const data = this.data()

      if (pages) {
        if (pages < data.totalPages) {
          for (let page = data.totalPages; page > pages; page--)
            this.turn('removePage', page)
        }

        data.totalPages = pages
        turnMethods._fitPage.call(this, data.page)

        return this
      }
      else {
        return data.totalPages
      }
    },

    // Checks missing pages

    _missing(page) {
      const data = this.data()

      if (data.totalPages < 1)
        return

      let p
      const range = this.turn('range', page)
      const missing = []

      for (p = range[0]; p <= range[1]; p++) {
        if (!data.pageObjs[p])
          missing.push(p)
      }

      if (missing.length > 0)
        this.trigger('missing', [missing])
    },

    // Sets a page without effect

    _fitPage(page) {
      const data = this.data()
      const newView = this.turn('view', page)

      turnMethods._missing.call(this, page)

      if (!data.pageObjs[page])
        return

      data.page = page

      this.turn('stop')

      for (let i = 0; i < newView.length; i++) {
        if (newView[i] && data.pageZoom[newView[i]] != data.zoom) {
          this.trigger('zoomed', [
            newView[i],
            newView,
            data.pageZoom[newView[i]],
            data.zoom,
          ])

          data.pageZoom[newView[i]] = data.zoom
        }
      }

      turnMethods._removeFromDOM.call(this)
      turnMethods._makeRange.call(this)
      turnMethods._updateShadow.call(this)
      this.trigger('turned', [page, newView])
      this.turn('update')

      if (data.opts.autoCenter)
        this.turn('center')
    },

    // Turns the page

    _turnPage(page) {
      let current
      let next
      const data = this.data()
      const place = data.pagePlace[page]
      const view = this.turn('view')
      const newView = this.turn('view', page)

      if (data.page != page) {
        const currentPage = data.page

        if (trigger('turning', this, [page, newView]) == 'prevented') {
          if (currentPage == data.page && $.inArray(place, data.pageMv) != -1)
            data.pages[place].flip('hideFoldedPage', true)

          return
        }

        if ($.inArray(1, newView) != -1)
          this.trigger('first')
        if ($.inArray(data.totalPages, newView) != -1)
          this.trigger('last')
      }

      if (data.display == 'single') {
        current = view[0]
        next = newView[0]
      }
      else if (view[1] && page > view[1]) {
        current = view[1]
        next = newView[0]
      }
      else if (view[0] && page < view[0]) {
        current = view[0]
        next = newView[1]
      }

      const optsCorners = data.opts.turnCorners.split(',')
      const flipData = data.pages[current].data().f
      const opts = flipData.opts
      const actualPoint = flipData.point

      turnMethods._missing.call(this, page)

      if (!data.pageObjs[page])
        return

      this.turn('stop')

      data.page = page

      turnMethods._makeRange.call(this)

      data.tpage = next

      if (opts.next != next) {
        opts.next = next
        opts.force = true
      }

      this.turn('update')

      flipData.point = actualPoint

      if (flipData.effect == 'hard') {
        if (data.direction == 'ltr') {
          data.pages[current].flip('turnPage', (page > current) ? 'r' : 'l')
        }
        else {
          data.pages[current].flip('turnPage', (page > current) ? 'l' : 'r')
        }
      }
      else {
        if (data.direction == 'ltr') {
          data.pages[current].flip('turnPage', optsCorners[(page > current) ? 1 : 0])
        }
        else {
          data.pages[current].flip('turnPage', optsCorners[(page > current) ? 0 : 1])
        }
      }
    },

    // Gets and sets a page

    page(page) {
      const data = this.data()

      if (page === undefined) {
        return data.page
      }
      else {
        if (!data.disabled && !data.destroying) {
          page = Number.parseInt(page, 10)

          if (page > 0 && page <= data.totalPages) {
            if (page != data.page) {
              if (!data.done || $.inArray(page, this.turn('view')) != -1)
                turnMethods._fitPage.call(this, page)
              else
                turnMethods._turnPage.call(this, page)
            }

            return this
          }
          else {
            throw turnError(`The page ${page} does not exist`)
          }
        }
      }
    },

    // Turns to the next view

    next() {
      return this.turn('page', Math.min(this.data().totalPages, turnMethods._view.call(this, this.data().page).pop() + 1))
    },

    // Turns to the previous view

    previous() {
      return this.turn('page', Math.max(1, turnMethods._view.call(this, this.data().page).shift() - 1))
    },

    // Shows a peeling corner

    peel(corner, animate) {
      const data = this.data()
      const view = this.turn('view')

      animate = (animate === undefined) ? true : animate === true

      if (corner === false) {
        this.turn('stop', null, animate)
      }
      else {
        if (data.display == 'single') {
          data.pages[data.page].flip('peel', corner, animate)
        }
        else {
          let page

          if (data.direction == 'ltr') {
            page = (corner.includes('l')) ? view[0] : view[1]
          }
          else {
            page = (corner.includes('l')) ? view[1] : view[0]
          }

          if (data.pages[page])
            data.pages[page].flip('peel', corner, animate)
        }
      }

      return this
    },

    // Adds a motion to the internal list
    // This event is called in context of flip

    _addMotionPage() {
      const opts = $(this).data().f.opts
      const turn = opts.turn
      const dd = turn.data()

      turnMethods._addMv.call(turn, opts.page)
    },

    // This event is called in context of flip

    _eventStart(e, opts, corner) {
      const data = opts.turn.data()
      const actualZoom = data.pageZoom[opts.page]

      if (e.isDefaultPrevented()) {
        turnMethods._updateShadow.call(opts.turn)
        return
      }

      if (actualZoom && actualZoom != data.zoom) {
        opts.turn.trigger('zoomed', [
          opts.page,
          opts.turn.turn('view', opts.page),
          actualZoom,
          data.zoom,
        ])

        data.pageZoom[opts.page] = data.zoom
      }

      if (data.display == 'single' && corner) {
        if ((corner.charAt(1) == 'l' && data.direction == 'ltr')
          || (corner.charAt(1) == 'r' && data.direction == 'rtl')) {
          opts.next = (opts.next < opts.page) ? opts.next : opts.page - 1
          opts.force = true
        }
        else {
          opts.next = (opts.next > opts.page) ? opts.next : opts.page + 1
        }
      }

      turnMethods._addMotionPage.call(e.target)
      turnMethods._updateShadow.call(opts.turn)
    },

    // This event is called in context of flip

    _eventEnd(e, opts, turned) {
      const that = $(e.target)
      const data = that.data().f
      const turn = opts.turn
      const dd = turn.data()

      if (turned) {
        const tpage = dd.tpage || dd.page

        if (tpage == opts.next || tpage == opts.page) {
          delete dd.tpage

          turnMethods._fitPage.call(turn, tpage || opts.next, true)
        }
      }
      else {
        turnMethods._removeMv.call(turn, opts.page)
        turnMethods._updateShadow.call(turn)
        turn.turn('update')
      }
    },

    // This event is called in context of flip

    _eventPressed(e) {
      let page
      const data = $(e.target).data().f
      const turn = data.opts.turn
      const turnData = turn.data()
      const pages = turnData.pages

      turnData.mouseAction = true

      turn.turn('update')

      return data.time = new Date().getTime()
    },

    // This event is called in context of flip

    _eventReleased(e, point) {
      let outArea
      const page = $(e.target)
      const data = page.data().f
      const turn = data.opts.turn
      const turnData = turn.data()

      if (turnData.display == 'single') {
        outArea = (point.corner == 'br' || point.corner == 'tr')
          ? point.x < page.width() / 2
          : point.x > page.width() / 2
      }
      else {
        outArea = point.x < 0 || point.x > page.width()
      }

      if ((new Date()).getTime() - data.time < 200 || outArea) {
        e.preventDefault()
        turnMethods._turnPage.call(turn, data.opts.next)
      }

      turnData.mouseAction = false
    },

    // This event is called in context of flip

    _flip(e) {
      e.stopPropagation()

      const opts = $(e.target).data().f.opts

      opts.turn.trigger('turn', [opts.next])

      if (opts.turn.data().opts.autoCenter) {
        opts.turn.turn('center', opts.next)
      }
    },

    //
    _touchStart() {
      const data = this.data()
      for (const page in data.pages) {
        if (has(page, data.pages)
          && flipMethods._eventStart.apply(data.pages[page], arguments) === false) {
          return false
        }
      }
    },

    //
    _touchMove() {
      const data = this.data()
      for (const page in data.pages) {
        if (has(page, data.pages)) {
          flipMethods._eventMove.apply(data.pages[page], arguments)
        }
      }
    },

    //
    _touchEnd() {
      const data = this.data()
      for (const page in data.pages) {
        if (has(page, data.pages)) {
          flipMethods._eventEnd.apply(data.pages[page], arguments)
        }
      }
    },

    // Calculate the z-index value for pages during the animation

    calculateZ(mv) {
      let i; let page; let nextPage; let placePage; let dpage
      const that = this
      const data = this.data()
      const view = this.turn('view')
      const currentPage = view[0] || view[1]
      const total = mv.length - 1
      const r = { pageZ: {}, partZ: {}, pageV: {} }

      const addView = function (page) {
        const view = that.turn('view', page)
        if (view[0])
          r.pageV[view[0]] = true
        if (view[1])
          r.pageV[view[1]] = true
      }

      for (i = 0; i <= total; i++) {
        page = mv[i]
        nextPage = data.pages[page].data().f.opts.next
        placePage = data.pagePlace[page]
        addView(page)
        addView(nextPage)
        dpage = (data.pagePlace[nextPage] == nextPage) ? nextPage : page
        r.pageZ[dpage] = data.totalPages - Math.abs(currentPage - dpage)
        r.partZ[placePage] = data.totalPages * 2 - total + i
      }

      return r
    },

    // Updates the z-index and display property of every page

    update() {
      let page
      const data = this.data()

      if (this.turn('animating') && data.pageMv[0] !== 0) {
        // Update motion

        let p; let apage; let fixed
        const pos = this.turn('calculateZ', data.pageMv)
        const corner = this.turn('corner')
        const actualView = this.turn('view')
        const newView = this.turn('view', data.tpage)

        for (page in data.pageWrap) {
          if (!has(page, data.pageWrap))
            continue

          fixed = data.pageObjs[page].hasClass('fixed')

          data.pageWrap[page].css({
            display: (pos.pageV[page] || fixed) ? '' : 'none',
            zIndex:
              (data.pageObjs[page].hasClass('hard')
                ? pos.partZ[page]
                : pos.pageZ[page]
              ) || (fixed ? -1 : 0),
          })

          if ((p = data.pages[page])) {
            p.flip('z', pos.partZ[page] || null)

            if (pos.pageV[page])
              p.flip('resize')

            if (data.tpage) { // Is it turning the page to `tpage`?
              p.flip('hover', false)
                .flip('disable', $.inArray(Number.parseInt(page, 10), data.pageMv) == -1
                && page != newView[0]
                && page != newView[1])
            }
            else {
              p.flip('hover', corner === false)
                .flip('disable', page != actualView[0] && page != actualView[1])
            }
          }
        }
      }
      else {
        // Update static pages

        for (page in data.pageWrap) {
          if (!has(page, data.pageWrap))
            continue

          const pageLocation = turnMethods._setPageLoc.call(this, page)

          if (data.pages[page]) {
            data.pages[page]
              .flip('disable', data.disabled || pageLocation != 1)
              .flip('hover', true)
              .flip('z', null)
          }
        }
      }

      return this
    },

    // Updates the position and size of the flipbook's shadow

    _updateShadow() {
      let view; let view2; let shadow
      const data = this.data()
      const width = this.width()
      const height = this.height()
      const pageWidth = (data.display == 'single') ? width : width / 2

      view = this.turn('view')

      if (!data.shadow) {
        data.shadow = $('<div />', {
          class: 'shadow',
          css: divAtt(0, 0, 0).css,
        })
          .appendTo(this)
      }

      for (let i = 0; i < data.pageMv.length; i++) {
        if (!view[0] || !view[1])
          break

        view = this.turn('view', data.pages[data.pageMv[i]].data().f.opts.next)
        view2 = this.turn('view', data.pageMv[i])

        view[0] = view[0] && view2[0]
        view[1] = view[1] && view2[1]
      }

      if (!view[0])
        shadow = (data.direction == 'ltr') ? 1 : 2
      else if (!view[1])
        shadow = (data.direction == 'ltr') ? 2 : 1
      else shadow = 3

      switch (shadow) {
        case 1:
          data.shadow.css({
            width: pageWidth,
            height,
            top: 0,
            left: pageWidth,
          })
          break
        case 2:
          data.shadow.css({
            width: pageWidth,
            height,
            top: 0,
            left: 0,
          })
          break
        case 3:
          data.shadow.css({
            width,
            height,
            top: 0,
            left: 0,
          })
          break
      }
    },

    // Sets the z-index and display property of a page
    // It depends on the current view

    _setPageLoc(page) {
      const data = this.data()
      const view = this.turn('view')
      let loc = 0

      if (page == view[0] || page == view[1]) {
        loc = 1
      }
      else if (
        (data.display == 'single' && page == view[0] + 1)
        || (data.display == 'double' && page == view[0] - 2 || page == view[1] + 2)
      ) {
        loc = 2
      }

      if (!this.turn('animating')) {
        switch (loc) {
          case 1:
            data.pageWrap[page].css(
              {
                zIndex: data.totalPages,
                display: '',
              },
            )
            break
          case 2:
            data.pageWrap[page].css(
              {
                zIndex: data.totalPages - 1,
                display: '',
              },
            )
            break
          case 0:
            data.pageWrap[page].css(
              {
                zIndex: 0,
                display: (data.pageObjs[page].hasClass('fixed')) ? '' : 'none',
              },
            )
            break
        }
      }

      return loc
    },

    // Gets and sets the options

    options(options) {
      if (options === undefined) {
        return this.data().opts
      }
      else {
        const data = this.data()

        // Set new values

        $.extend(data.opts, options)

        // Set pages

        if (options.pages)
          this.turn('pages', options.pages)

        // Set page

        if (options.page)
          this.turn('page', options.page)

        // Set display

        if (options.display)
          this.turn('display', options.display)

        // Set direction

        if (options.direction)
          this.turn('direction', options.direction)

        // Set size

        if (options.width && options.height)
          this.turn('size', options.width, options.height)

        // Add event listeners

        if (options.when) {
          for (const eventName in options.when) {
            if (has(eventName, options.when)) {
              this.unbind(eventName)
                .bind(eventName, options.when[eventName])
            }
          }
        }

        return this
      }
    },

    // Gets the current version

    version() {
      return version
    },
  }

  // Methods and properties for the flip page effect

  var flipMethods = {

    // Constructor

    init(opts) {
      this.data({ f: {
        disabled: false,
        hover: false,
        effect: (this.hasClass('hard')) ? 'hard' : 'sheet',
      } })

      this.flip('options', opts)

      flipMethods._addPageWrapper.call(this)

      return this
    },

    setData(d) {
      const data = this.data()

      data.f = $.extend(data.f, d)

      return this
    },

    options(opts) {
      const data = this.data().f

      if (opts) {
        flipMethods.setData.call(this, { opts: $.extend({}, data.opts || flipOptions, opts) })
        return this
      }
      else {
        return data.opts
      }
    },

    z(z) {
      const data = this.data().f

      data.opts['z-index'] = z

      if (data.fwrapper) {
        data.fwrapper.css({
          zIndex: z || Number.parseInt(data.parent.css('z-index'), 10) || 0,
        })
      }

      return this
    },

    _cAllowed() {
      const data = this.data().f
      const page = data.opts.page
      const turnData = data.opts.turn.data()
      const odd = page % 2

      if (data.effect == 'hard') {
        return (turnData.direction == 'ltr')
          ? [(odd) ? 'r' : 'l']
          : [(odd) ? 'l' : 'r']
      }
      else {
        if (turnData.display == 'single') {
          if (page == 1) {
            return (turnData.direction == 'ltr')
              ? corners.forward
              : corners.backward
          }
          else if (page == turnData.totalPages) {
            return (turnData.direction == 'ltr')
              ? corners.backward
              : corners.forward
          }
          else {
            return corners.all
          }
        }
        else {
          return (turnData.direction == 'ltr')
            ? corners[(odd) ? 'forward' : 'backward']
            : corners[(odd) ? 'backward' : 'forward']
        }
      }
    },

    _cornerActivated(p) {
      const data = this.data().f
      const width = this.width()
      const height = this.height()
      const point = { x: p.x, y: p.y, corner: '' }
      const csz = data.opts.cornerSize

      if (point.x <= 0 || point.y <= 0 || point.x >= width || point.y >= height)
        return false

      const allowedCorners = flipMethods._cAllowed.call(this)

      switch (data.effect) {
        case 'hard':

          if (point.x > width - csz)
            point.corner = 'r'
          else if (point.x < csz)
            point.corner = 'l'
          else
            return false

          break

        case 'sheet':

          if (point.y < csz)
            point.corner += 't'
          else if (point.y >= height - csz)
            point.corner += 'b'
          else
            return false

          if (point.x <= csz)
            point.corner += 'l'
          else if (point.x >= width - csz)
            point.corner += 'r'
          else
            return false

          break
      }

      return (!point.corner || $.inArray(point.corner, allowedCorners) == -1)
        ? false
        : point
    },

    _isIArea(e) {
      const pos = this.data().f.parent.offset()

      e = (isTouch && e.originalEvent) ? e.originalEvent.touches[0] : e

      return flipMethods._cornerActivated.call(this, {
        x: e.pageX - pos.left,
        y: e.pageY - pos.top,
      })
    },

    _c(corner, opts) {
      opts = opts || 0

      switch (corner) {
        case 'tl':
          return point2D(opts, opts)
        case 'tr':
          return point2D(this.width() - opts, opts)
        case 'bl':
          return point2D(opts, this.height() - opts)
        case 'br':
          return point2D(this.width() - opts, this.height() - opts)
        case 'l':
          return point2D(opts, 0)
        case 'r':
          return point2D(this.width() - opts, 0)
      }
    },

    _c2(corner) {
      switch (corner) {
        case 'tl':
          return point2D(this.width() * 2, 0)
        case 'tr':
          return point2D(-this.width(), 0)
        case 'bl':
          return point2D(this.width() * 2, this.height())
        case 'br':
          return point2D(-this.width(), this.height())
        case 'l':
          return point2D(this.width() * 2, 0)
        case 'r':
          return point2D(-this.width(), 0)
      }
    },

    _foldingPage() {
      let data = this.data().f

      if (!data)
        return

      const opts = data.opts

      if (opts.turn) {
        data = opts.turn.data()
        if (data.display == 'single')
          return (opts.next > 1 || opts.page > 1) ? data.pageObjs[0] : null
        else
          return data.pageObjs[opts.next]
      }
    },

    _backGradient() {
      const data = this.data().f
      const turnData = data.opts.turn.data()
      const gradient = turnData.opts.gradients && (turnData.display == 'single'
        || (data.opts.page != 2 && data.opts.page != turnData.totalPages - 1))

      if (gradient && !data.bshadow) {
        data.bshadow = $('<div/>', divAtt(0, 0, 1))
          .css({ position: '', width: this.width(), height: this.height() })
          .appendTo(data.parent)
      }

      return gradient
    },

    type() {
      return this.data().f.effect
    },

    resize(full) {
      const data = this.data().f
      const turnData = data.opts.turn.data()
      const width = this.width()
      const height = this.height()

      switch (data.effect) {
        case 'hard':

          if (full) {
            data.wrapper.css({ width, height })
            data.fpage.css({ width, height })
            if (turnData.opts.gradients) {
              data.ashadow.css({ width, height })
              data.bshadow.css({ width, height })
            }
          }

          break
        case 'sheet':

          if (full) {
            const size = Math.round(Math.sqrt(width ** 2 + height ** 2))

            data.wrapper.css({ width: size, height: size })
            data.fwrapper.css({ width: size, height: size })
              .children(':first-child')
              .css({ width, height })

            data.fpage.css({ width, height })

            if (turnData.opts.gradients)
              data.ashadow.css({ width, height })

            if (flipMethods._backGradient.call(this))
              data.bshadow.css({ width, height })
          }

          if (data.parent.is(':visible')) {
            let offset = findPos(data.parent[0])

            data.fwrapper.css({ top: offset.top, left: offset.left })

            // if (data.opts.turn) {
            offset = findPos(data.opts.turn[0])
            data.fparent.css({ top: -offset.top, left: -offset.left })
          // }
          }

          this.flip('z', data.opts['z-index'])

          break
      }
    },

    // Prepares the page by adding a general wrapper and another objects

    _addPageWrapper() {
      let att
      const data = this.data().f
      const turnData = data.opts.turn.data()
      const parent = this.parent()

      data.parent = parent

      if (!data.wrapper) {
        switch (data.effect) {
          case 'hard':

            var cssProperties = {}
            cssProperties[`${vendor}transform-style`] = 'preserve-3d'
            cssProperties[`${vendor}backface-visibility`] = 'hidden'

            data.wrapper = $('<div/>', divAtt(0, 0, 2))
              .css(cssProperties)
              .appendTo(parent)
              .prepend(this)

            data.fpage = $('<div/>', divAtt(0, 0, 1))
              .css(cssProperties)
              .appendTo(parent)

            if (turnData.opts.gradients) {
              data.ashadow = $('<div/>', divAtt(0, 0, 0))
                .hide()
                .appendTo(parent)

              data.bshadow = $('<div/>', divAtt(0, 0, 0))
            }

            break
          case 'sheet':

            var width = this.width()
            var height = this.height()
            var size = Math.round(Math.sqrt(width ** 2 + height ** 2))

            data.fparent = data.opts.turn.data().fparent

            if (!data.fparent) {
              const fparent = $('<div/>', { css: { 'pointer-events': 'none' } }).hide()
              fparent.data().flips = 0
              fparent.css(divAtt(0, 0, 'auto', 'visible').css)
                .appendTo(data.opts.turn)

              data.opts.turn.data().fparent = fparent
              data.fparent = fparent
            }

            this.css({ position: 'absolute', top: 0, left: 0, bottom: 'auto', right: 'auto' })

            data.wrapper = $('<div/>', divAtt(0, 0, this.css('z-index')))
              .appendTo(parent)
              .prepend(this)

            data.fwrapper = $('<div/>', divAtt(parent.offset().top, parent.offset().left))
              .hide()
              .appendTo(data.fparent)

            data.fpage = $('<div/>', divAtt(0, 0, 0, 'visible'))
              .css({ cursor: 'default' })
              .appendTo(data.fwrapper)

            if (turnData.opts.gradients) {
              data.ashadow = $('<div/>', divAtt(0, 0, 1))
                .appendTo(data.fpage)
            }

            flipMethods.setData.call(this, data)

            break
        }
      }

      // Set size
      flipMethods.resize.call(this, true)
    },

    // Takes a 2P point from the screen and applies the transformation

    _fold(point) {
      const data = this.data().f
      const turnData = data.opts.turn.data()
      const o = flipMethods._c.call(this, point.corner)
      let width = this.width()
      const height = this.height()

      switch (data.effect) {
        case 'hard':

          if (point.corner == 'l')
            point.x = Math.min(Math.max(point.x, 0), width * 2)
          else
            point.x = Math.max(Math.min(point.x, width), -width)

          var leftPos
          var shadow
          var gradientX
          var fpageOrigin
          var parentOrigin
          var totalPages = turnData.totalPages
          var zIndex = data.opts['z-index'] || totalPages
          var parentCss = { overflow: 'visible' }
          var relX = (o.x) ? (o.x - point.x) / width : point.x / width
          var angle = relX * 90
          var half = angle < 90

          switch (point.corner) {
            case 'l':

              fpageOrigin = '0% 50%'
              parentOrigin = '100% 50%'

              if (half) {
                leftPos = 0
                shadow = data.opts.next - 1 > 0
                gradientX = 1
              }
              else {
                leftPos = '100%'
                shadow = data.opts.page + 1 < totalPages
                gradientX = 0
              }

              break
            case 'r':

              fpageOrigin = '100% 50%'
              parentOrigin = '0% 50%'
              angle = -angle
              width = -width

              if (half) {
                leftPos = 0
                shadow = data.opts.next + 1 < totalPages
                gradientX = 0
              }
              else {
                leftPos = '-100%'
                shadow = data.opts.page != 1
                gradientX = 1
              }

              break
          }

          parentCss[`${vendor}perspective-origin`] = parentOrigin

          data.wrapper.transform(`rotateY(${angle}deg)`
            + `translate3d(0px, 0px, ${this.attr('depth') || 0}px)`, parentOrigin)

          data.fpage.transform(`translateX(${width}px) rotateY(${180 + angle}deg)`, fpageOrigin)

          data.parent.css(parentCss)

          if (half) {
            relX = -relX + 1
            data.wrapper.css({ zIndex: zIndex + 1 })
            data.fpage.css({ zIndex })
          }
          else {
            relX = relX - 1
            data.wrapper.css({ zIndex })
            data.fpage.css({ zIndex: zIndex + 1 })
          }

          if (turnData.opts.gradients) {
            if (shadow) {
              data.ashadow.css({
                display: '',
                left: leftPos,
                backgroundColor: `rgba(0,0,0,${0.5 * relX})`,
              })
                .transform('rotateY(0deg)')
            }
            else {
              data.ashadow.hide()
            }

            data.bshadow.css({ opacity: -relX + 1 })

            if (half) {
              if (data.bshadow.parent()[0] != data.wrapper[0]) {
                data.bshadow.appendTo(data.wrapper)
              }
            }
            else {
              if (data.bshadow.parent()[0] != data.fpage[0]) {
                data.bshadow.appendTo(data.fpage)
              }
            }
            /* data.bshadow.css({
              backgroundColor: 'rgba(0,0,0,'+(0.1)+')'
            }) */
            gradient(data.bshadow, point2D(gradientX * 100, 0), point2D((-gradientX + 1) * 100, 0), [[0, 'rgba(0,0,0,0.3)'], [1, 'rgba(0,0,0,0)']], 2)
          }

          break
        case 'sheet':

          var that = this
          var a = 0
          var alpha = 0
          var beta
          var px
          var gradientEndPointA
          var gradientEndPointB
          var gradientStartVal
          var gradientSize
          var gradientOpacity
          var shadowVal
          var mv = point2D(0, 0)
          var df = point2D(0, 0)
          var tr = point2D(0, 0)
          var folding = flipMethods._foldingPage.call(this)
          var tan = Math.tan(alpha)
          var ac = turnData.opts.acceleration
          var h = data.wrapper.height()
          var top = point.corner.substr(0, 1) == 't'
          var left = point.corner.substr(1, 1) == 'l'

          var compute = function () {
            const rel = point2D(0, 0)
            const middle = point2D(0, 0)

            rel.x = (o.x) ? o.x - point.x : point.x

            if (!hasRot) {
              rel.y = 0
            }
            else {
              rel.y = (o.y) ? o.y - point.y : point.y
            }

            middle.x = (left) ? width - rel.x / 2 : point.x + rel.x / 2
            middle.y = rel.y / 2

            const alpha = A90 - Math.atan2(rel.y, rel.x)
            const gamma = alpha - Math.atan2(middle.y, middle.x)
            const distance = Math.max(0, Math.sin(gamma) * Math.sqrt(middle.x ** 2 + middle.y ** 2))

            a = deg(alpha)

            tr = point2D(distance * Math.sin(alpha), distance * Math.cos(alpha))

            if (alpha > A90) {
              tr.x = tr.x + Math.abs(tr.y * rel.y / rel.x)
              tr.y = 0
              if (Math.round(tr.x * Math.tan(PI - alpha)) < height) {
                point.y = Math.sqrt(height ** 2 + 2 * middle.x * rel.x)
                if (top)
                  point.y = height - point.y
                return compute()
              }
            }

            if (alpha > A90) {
              const beta = PI - alpha; const dd = h - height / Math.sin(beta)
              mv = point2D(Math.round(dd * Math.cos(beta)), Math.round(dd * Math.sin(beta)))
              if (left)
                mv.x = -mv.x
              if (top)
                mv.y = -mv.y
            }

            px = Math.round(tr.y / Math.tan(alpha) + tr.x)

            const side = width - px
            const sideX = side * Math.cos(alpha * 2)
            const sideY = side * Math.sin(alpha * 2)
            df = point2D(
              Math.round((left ? side - sideX : px + sideX)),
              Math.round((top) ? sideY : height - sideY),
            )

            // Gradients
            if (turnData.opts.gradients) {
              gradientSize = side * Math.sin(alpha)

              const endingPoint = flipMethods._c2.call(that, point.corner)
              const far = Math.sqrt((endingPoint.x - point.x) ** 2 + (endingPoint.y - point.y) ** 2) / width

              shadowVal = Math.sin(A90 * ((far > 1) ? 2 - far : far))

              gradientOpacity = Math.min(far, 1)

              gradientStartVal = gradientSize > 100 ? (gradientSize - 100) / gradientSize : 0

              gradientEndPointA = point2D(
                gradientSize * Math.sin(alpha) / width * 100,
                gradientSize * Math.cos(alpha) / height * 100,
              )

              if (flipMethods._backGradient.call(that)) {
                gradientEndPointB = point2D(
                  gradientSize * 1.2 * Math.sin(alpha) / width * 100,
                  gradientSize * 1.2 * Math.cos(alpha) / height * 100,
                )

                if (!left)
                  gradientEndPointB.x = 100 - gradientEndPointB.x
                if (!top)
                  gradientEndPointB.y = 100 - gradientEndPointB.y
              }
            }

            tr.x = Math.round(tr.x)
            tr.y = Math.round(tr.y)

            return true
          }

          var transform = function (tr, c, x, a) {
            const f = ['0', 'auto']; const mvW = (width - h) * x[0] / 100; const mvH = (height - h) * x[1] / 100
            const cssA = { left: f[c[0]], top: f[c[1]], right: f[c[2]], bottom: f[c[3]] }
            const cssB = {}
            const aliasingFk = (a != 90 && a != -90) ? (left ? -1 : 1) : 0
            const origin = `${x[0]}% ${x[1]}%`

            that.css(cssA)
              .transform(rotate(a) + translate(tr.x + aliasingFk, tr.y, ac), origin)

            data.fpage.css(cssA).transform(
              rotate(a)
              + translate(tr.x + df.x - mv.x - width * x[0] / 100, tr.y + df.y - mv.y - height * x[1] / 100, ac)
              + rotate((180 / a - 2) * a),
              origin,
            )

            data.wrapper.transform(translate(-tr.x + mvW - aliasingFk, -tr.y + mvH, ac) + rotate(-a), origin)

            data.fwrapper.transform(translate(-tr.x + mv.x + mvW, -tr.y + mv.y + mvH, ac) + rotate(-a), origin)

            if (turnData.opts.gradients) {
              if (x[0])
                gradientEndPointA.x = 100 - gradientEndPointA.x

              if (x[1])
                gradientEndPointA.y = (100 - gradientEndPointA.y)

              cssB['box-shadow'] = `0 0 20px rgba(0,0,0,${0.5 * shadowVal})`
              folding.css(cssB)

              gradient(data.ashadow, point2D(left ? 100 : 0, top ? 0 : 100), point2D(gradientEndPointA.x, gradientEndPointA.y), [[gradientStartVal, 'rgba(0,0,0,0)'], [((1 - gradientStartVal) * 0.8) + gradientStartVal, `rgba(0,0,0,${0.2 * gradientOpacity})`], [1, `rgba(255,255,255,${0.2 * gradientOpacity})`]], 3, alpha)

              if (flipMethods._backGradient.call(that)) {
                gradient(data.bshadow, point2D(left ? 0 : 100, top ? 0 : 100), point2D(gradientEndPointB.x, gradientEndPointB.y), [[0.6, 'rgba(0,0,0,0)'], [0.8, `rgba(0,0,0,${0.3 * gradientOpacity})`], [1, 'rgba(0,0,0,0)'],
                ], 3)
              }
            }
          }

          switch (point.corner) {
            case 'l' :

              break
            case 'r' :

              break
            case 'tl' :
              point.x = Math.max(point.x, 1)
              compute()
              transform(tr, [1, 0, 0, 1], [100, 0], a)
              break
            case 'tr' :
              point.x = Math.min(point.x, width - 1)
              compute()
              transform(point2D(-tr.x, tr.y), [0, 0, 0, 1], [0, 0], -a)
              break
            case 'bl' :
              point.x = Math.max(point.x, 1)
              compute()
              transform(point2D(tr.x, -tr.y), [1, 1, 0, 0], [100, 100], -a)
              break
            case 'br' :
              point.x = Math.min(point.x, width - 1)
              compute()
              transform(point2D(-tr.x, -tr.y), [0, 1, 1, 0], [0, 100], a)
              break
          }

          break
      }

      data.point = point
    },

    _moveFoldingPage(move) {
      const data = this.data().f

      if (!data)
        return

      const turn = data.opts.turn
      const turnData = turn.data()
      const place = turnData.pagePlace

      if (move) {
        const nextPage = data.opts.next

        if (place[nextPage] != data.opts.page) {
          if (data.folding)
            flipMethods._moveFoldingPage.call(this, false)

          const folding = flipMethods._foldingPage.call(this)

          folding.appendTo(data.fpage)
          place[nextPage] = data.opts.page
          data.folding = nextPage
        }

        turn.turn('update')
      }
      else {
        if (data.folding) {
          if (turnData.pages[data.folding]) {
            // If we have flip available

            const flipData = turnData.pages[data.folding].data().f

            turnData.pageObjs[data.folding]
              .appendTo(flipData.wrapper)
          }
          else if (turnData.pageWrap[data.folding]) {
            // If we have the pageWrapper

            turnData.pageObjs[data.folding]
              .appendTo(turnData.pageWrap[data.folding])
          }

          if (data.folding in place) {
            place[data.folding] = data.folding
          }

          delete data.folding
        }
      }
    },

    _showFoldedPage(c, animate) {
      const folding = flipMethods._foldingPage.call(this)
      const dd = this.data()
      const data = dd.f
      let visible = data.visible

      if (folding) {
        if (!visible || !data.point || data.point.corner != c.corner) {
          const corner = (
            data.status == 'hover'
            || data.status == 'peel'
            || data.opts.turn.data().mouseAction)
            ? c.corner
            : null

          visible = false

          if (trigger('start', this, [data.opts, corner]) == 'prevented')
            return false
        }

        if (animate) {
          const that = this
          const point = (data.point && data.point.corner == c.corner)
            ? data.point
            : flipMethods._c.call(this, c.corner, 1)

          this.animatef({
            from: [point.x, point.y],
            to: [c.x, c.y],
            duration: 500,
            frame(v) {
              c.x = Math.round(v[0])
              c.y = Math.round(v[1])
              flipMethods._fold.call(that, c)
            },
          })
        }
        else {
          flipMethods._fold.call(this, c)

          if (dd.effect && !dd.effect.turning)
            this.animatef(false)
        }

        if (!visible) {
          switch (data.effect) {
            case 'hard':

              data.visible = true
              flipMethods._moveFoldingPage.call(this, true)
              data.fpage.show()
              if (data.opts.shadows)
                data.bshadow.show()

              break
            case 'sheet':

              data.visible = true
              data.fparent.show().data().flips++
              flipMethods._moveFoldingPage.call(this, true)
              data.fwrapper.show()
              if (data.bshadow)
                data.bshadow.show()

              break
          }
        }

        return true
      }

      return false
    },

    hide() {
      const data = this.data().f
      const turnData = data.opts.turn.data()
      const folding = flipMethods._foldingPage.call(this)

      switch (data.effect) {
        case 'hard':

          if (turnData.opts.gradients) {
            data.bshadowLoc = 0
            data.bshadow.remove()
            data.ashadow.hide()
          }

          data.wrapper.transform('')
          data.fpage.hide()

          break
        case 'sheet':

          if ((--data.fparent.data().flips) === 0)
            data.fparent.hide()

          this.css({ left: 0, top: 0, right: 'auto', bottom: 'auto' })
            .transform('')

          data.wrapper.transform('')

          data.fwrapper.hide()

          if (data.bshadow)
            data.bshadow.hide()

          folding.transform('')

          break
      }

      data.visible = false

      return this
    },

    hideFoldedPage(animate) {
      const data = this.data().f

      if (!data.point)
        return

      const that = this
      const p1 = data.point
      const hide = function () {
        data.point = null
        data.status = ''
        that.flip('hide')
        that.trigger('end', [data.opts, false])
      }

      if (animate) {
        const p4 = flipMethods._c.call(this, p1.corner)
        const top = (p1.corner.substr(0, 1) == 't')
        const delta = (top) ? Math.min(0, p1.y - p4.y) / 2 : Math.max(0, p1.y - p4.y) / 2
        const p2 = point2D(p1.x, p1.y + delta)
        const p3 = point2D(p4.x, p4.y - delta)

        this.animatef({
          from: 0,
          to: 1,
          frame(v) {
            const np = bezier(p1, p2, p3, p4, v)
            p1.x = np.x
            p1.y = np.y
            flipMethods._fold.call(that, p1)
          },
          complete: hide,
          duration: 800,
          hiding: true,
        })
      }
      else {
        this.animatef(false)
        hide()
      }
    },

    turnPage(corner) {
      const that = this
      const data = this.data().f
      const turnData = data.opts.turn.data()

      corner = { corner: (data.corner)
        ? data.corner.corner
        : corner || flipMethods._cAllowed.call(this)[0] }

      const p1 = data.point
        || flipMethods._c.call(this, corner.corner, (data.opts.turn) ? turnData.opts.elevation : 0)
      const p4 = flipMethods._c2.call(this, corner.corner)

      this.trigger('flip')
        .animatef({
          from: 0,
          to: 1,
          frame(v) {
            const np = bezier(p1, p1, p4, p4, v)
            corner.x = np.x
            corner.y = np.y
            flipMethods._showFoldedPage.call(that, corner)
          },
          complete() {
            that.trigger('end', [data.opts, true])
          },
          duration: turnData.opts.duration,
          turning: true,
        })

      data.corner = null
    },

    moving() {
      return 'effect' in this.data()
    },

    isTurning() {
      return this.flip('moving') && this.data().effect.turning
    },

    corner() {
      return this.data().f.corner
    },

    _eventStart(e) {
      const data = this.data().f
      const turn = data.opts.turn

      if (!data.corner && !data.disabled && !this.flip('isTurning')
        && data.opts.page == turn.data().pagePlace[data.opts.page]) {
        data.corner = flipMethods._isIArea.call(this, e)

        if (data.corner && flipMethods._foldingPage.call(this)) {
          this.trigger('pressed', [data.point])
          flipMethods._showFoldedPage.call(this, data.corner)

          return false
        }
        else {
          data.corner = null
        }
      }
    },

    _eventMove(e) {
      const data = this.data().f

      if (!data.disabled) {
        e = (isTouch) ? e.originalEvent.touches : [e]

        if (data.corner) {
          const pos = data.parent.offset()
          data.corner.x = e[0].pageX - pos.left
          data.corner.y = e[0].pageY - pos.top
          flipMethods._showFoldedPage.call(this, data.corner)
        }
        else if (data.hover && !this.data().effect && this.is(':visible')) {
          const point = flipMethods._isIArea.call(this, e[0])

          if (point) {
            if ((data.effect == 'sheet' && point.corner.length == 2) || data.effect == 'hard') {
              data.status = 'hover'
              const origin = flipMethods._c.call(this, point.corner, data.opts.cornerSize / 2)
              point.x = origin.x
              point.y = origin.y
              flipMethods._showFoldedPage.call(this, point, true)
            }
          }
          else {
            if (data.status == 'hover') {
              data.status = ''
              flipMethods.hideFoldedPage.call(this, true)
            }
          }
        }
      }
    },

    _eventEnd() {
      const data = this.data().f
      const corner = data.corner

      if (!data.disabled && corner) {
        if (trigger('released', this, [data.point || corner]) != 'prevented') {
          flipMethods.hideFoldedPage.call(this, true)
        }
      }

      data.corner = null
    },

    disable(disable) {
      flipMethods.setData.call(this, { disabled: disable })
      return this
    },

    hover(hover) {
      flipMethods.setData.call(this, { hover })
      return this
    },

    peel(corner, animate) {
      const data = this.data().f

      if (corner) {
        if ($.inArray(corner, corners.all) == -1)
          throw turnError(`Corner ${corner} is not permitted`)

        if ($.inArray(corner, flipMethods._cAllowed.call(this)) != -1) {
          const point = flipMethods._c.call(this, corner, data.opts.cornerSize / 2)

          data.status = 'peel'

          flipMethods._showFoldedPage.call(this, {
            corner,
            x: point.x,
            y: point.y,
          }, animate)
        }
      }
      else {
        data.status = ''

        flipMethods.hideFoldedPage.call(this, animate)
      }

      return this
    },
  }

  // Processes classes

  function dec(that, methods, args) {
    if (!args[0] || typeof (args[0]) == 'object')
      return methods.init.apply(that, args)

    else if (methods[args[0]])
      return methods[args[0]].apply(that, Array.prototype.slice.call(args, 1))

    else
      throw turnError(`${args[0]} is not a method or property`)
  }

  // Attributes for a layer

  function divAtt(top, left, zIndex, overf) {
    return { css: {
      position: 'absolute',
      top,
      left,
      overflow: overf || 'hidden',
      zIndex: zIndex || 'auto',
    },
    }
  }

  // Gets a 2D point from a bezier curve of four points

  function bezier(p1, p2, p3, p4, t) {
    const a = 1 - t
    const b = a * a * a
    const c = t * t * t

    return point2D(Math.round(b * p1.x + 3 * t * a * a * p2.x + 3 * t * t * a * p3.x + c * p4.x), Math.round(b * p1.y + 3 * t * a * a * p2.y + 3 * t * t * a * p3.y + c * p4.y))
  }

  // Converts an angle from degrees to radians

  function rad(degrees) {
    return degrees / 180 * PI
  }

  // Converts an angle from radians to degrees

  function deg(radians) {
    return radians / PI * 180
  }

  // Gets a 2D point

  function point2D(x, y) {
    return { x, y }
  }

  // Webkit 534.3 on Android wrongly repaints elements that use overflow:hidden + rotation

  function rotationAvailable() {
    let parts

    if ((parts = /AppleWebkit\/([0-9.]+)/i.exec(navigator.userAgent))) {
      const webkitVersion = Number.parseFloat(parts[1])
      return (webkitVersion > 534.3)
    }
    else {
      return true
    }
  }

  // Returns the traslate value

  function translate(x, y, use3d) {
    return (has3d && use3d)
      ? ` translate3d(${x}px,${y}px, 0px) `
      : ` translate(${x}px, ${y}px) `
  }

  // Returns the rotation value

  function rotate(degrees) {
    return ` rotate(${degrees}deg) `
  }

  // Checks if a property belongs to an object

  function has(property, object) {
    return Object.prototype.hasOwnProperty.call(object, property)
  }

  // Gets the CSS3 vendor prefix

  function getPrefix() {
    const vendorPrefixes = ['Moz', 'Webkit', 'Khtml', 'O', 'ms']
    let len = vendorPrefixes.length
    let vendor = ''

    while (len--) {
      if ((`${vendorPrefixes[len]}Transform`) in document.body.style)
        vendor = `-${vendorPrefixes[len].toLowerCase()}-`
    }

    return vendor
  }

  // Detects the transitionEnd Event

  function getTransitionEnd() {
    let t
    const el = document.createElement('fakeelement')
    const transitions = {
      transition: 'transitionend',
      OTransition: 'oTransitionEnd',
      MSTransition: 'transitionend',
      MozTransition: 'transitionend',
      WebkitTransition: 'webkitTransitionEnd',
    }

    for (t in transitions) {
      if (el.style[t] !== undefined) {
        return transitions[t]
      }
    }
  }

  // Gradients

  function gradient(obj, p0, p1, colors, numColors) {
    let j; const cols = []

    if (vendor == '-webkit-') {
      for (j = 0; j < numColors; j++)
        cols.push(`color-stop(${colors[j][0]}, ${colors[j][1]})`)

      obj.css({ 'background-image':
          `-webkit-gradient(linear, ${
            p0.x}% ${
            p0.y}%,${
            p1.x}% ${
            p1.y}%, ${
            cols.join(',')} )` })
    }
    else {
      p0 = { x: p0.x / 100 * obj.width(), y: p0.y / 100 * obj.height() }
      p1 = { x: p1.x / 100 * obj.width(), y: p1.y / 100 * obj.height() }

      const dx = p1.x - p0.x
      const dy = p1.y - p0.y
      const angle = Math.atan2(dy, dx)
      const angle2 = angle - Math.PI / 2
      const diagonal = Math.abs(obj.width() * Math.sin(angle2)) + Math.abs(obj.height() * Math.cos(angle2))
      const gradientDiagonal = Math.sqrt(dy * dy + dx * dx)
      const corner = point2D((p1.x < p0.x) ? obj.width() : 0, (p1.y < p0.y) ? obj.height() : 0)
      const slope = Math.tan(angle)
      const inverse = -1 / slope
      const x = (inverse * corner.x - corner.y - slope * p0.x + p0.y) / (inverse - slope)
      const c = { x, y: inverse * x - inverse * corner.x + corner.y }
      const segA = (Math.sqrt((c.x - p0.x) ** 2 + (c.y - p0.y) ** 2))

      for (j = 0; j < numColors; j++)
        cols.push(` ${colors[j][1]} ${(segA + gradientDiagonal * colors[j][0]) * 100 / diagonal}%`)

      obj.css({ 'background-image': `${vendor}linear-gradient(${-angle}rad,${cols.join(',')})` })
    }
  }

  // Triggers an event

  function trigger(eventName, context, args) {
    const event = $.Event(eventName)
    context.trigger(event, args)
    if (event.isDefaultPrevented())
      return 'prevented'
    else if (event.isPropagationStopped())
      return 'stopped'
    else
      return ''
  }

  // JS Errors

  function turnError(message) {
    function TurnJsError(message) {
      this.name = 'TurnJsError'
      this.message = message
    }

    TurnJsError.prototype = new Error()
    TurnJsError.prototype.constructor = TurnJsError
    return new TurnJsError(message)
  }

  // Find the offset of an element ignoring its transformation

  function findPos(obj) {
    const offset = { top: 0, left: 0 }

    do {
      offset.left += obj.offsetLeft
      offset.top += obj.offsetTop
    } while ((obj = obj.offsetParent))

    return offset
  }

  // Checks if there's hard page compatibility
  // IE9 is the only browser that does not support hard pages

  function hasHardPage() {
    return (!navigator.userAgent.includes('MSIE 9.0'))
  }

  // Request an animation

  window.requestAnim = (function () {
    return window.requestAnimationFrame
      || window.webkitRequestAnimationFrame
      || window.mozRequestAnimationFrame
      || window.oRequestAnimationFrame
      || window.msRequestAnimationFrame
      || function (callback) {
        window.setTimeout(callback, 1000 / 60)
      }
  })()

  // Extend $.fn

  $.extend($.fn, {

    flip() {
      return dec($(this[0]), flipMethods, arguments)
    },

    turn() {
      return dec($(this[0]), turnMethods, arguments)
    },

    transform(transform, origin) {
      const properties = {}

      if (origin)
        properties[`${vendor}transform-origin`] = origin

      properties[`${vendor}transform`] = transform

      return this.css(properties)
    },

    animatef(point) {
      const data = this.data()

      if (data.effect)
        data.effect.stop()

      if (point) {
        if (!point.to.length)
          point.to = [point.to]
        if (!point.from.length)
          point.from = [point.from]

        const diff = []
        const len = point.to.length
        let animating = true
        const that = this
        const time = (new Date()).getTime()
        const frame = function () {
          if (!data.effect || !animating)
            return

          const v = []
          const timeDiff = Math.min(point.duration, (new Date()).getTime() - time)

          for (let i = 0; i < len; i++)
            v.push(data.effect.easing(1, timeDiff, point.from[i], diff[i], point.duration))

          point.frame((len == 1) ? v[0] : v)

          if (timeDiff == point.duration) {
            delete data.effect
            that.data(data)
            if (point.complete)
              point.complete()
          }
          else {
            window.requestAnim(frame)
          }
        }

        for (let i = 0; i < len; i++)
          diff.push(point.to[i] - point.from[i])

        data.effect = $.extend({
          stop() {
            animating = false
          },
          easing(x, t, b, c, data) {
            return c * Math.sqrt(1 - (t = t / data - 1) * t) + b
          },
        }, point)

        this.data(data)

        frame()
      }
      else {
        delete data.effect
      }
    },
  })

  // Export some globals

  $.isTouch = isTouch
  $.mouseEvents = mouseEvents
  $.cssPrefix = getPrefix
  $.cssTransitionEnd = getTransitionEnd
  $.findPos = findPos
})(jQuery)
